package eu.webhib.webblog.domain;

import java.util.*;
import javax.persistence.*;
import org.apache.log4j.Logger;

import eu.webhib.frmwrk.utils.MiscUtils;
import eu.webhib.frmwrk.jsf.utils.JsfUtil;
import eu.webhib.webblog.domain.base.CommonBaseEntityImpl;

//
@javax.persistence.Entity
@javax.persistence.NamedQueries({
   @javax.persistence.NamedQuery(name = "User_All", query = "SELECT u FROM User u")
})
@Access(AccessType.FIELD)
public class User extends CommonBaseEntityImpl {

   private static final Logger log = Logger.getLogger(User.class);
   private static final long serialVersionUID = 1L;
   // length of the confirmation secret in bytes
   private static final short confirmSecretLen = 32;
   // length of the salt of password hash in bytes
   private static final short saltLen = 6;
   //
   private static final Character STATUS_REGISTRATION_PENDING = 'R';
   private static final Character STATUS_CONFIRMED = 'C';
   private static final Character STATUS_PASSWORD_RESET_PENDING = 'P';
   private static final Character STATUS_EMAIL_CHANGE_PENDIG = 'E';
   // attributes
   @Column(nullable = false, unique = true)
   private String displayName;
   @Column(nullable = false)
   private String email;
   @Column
   private String emailConfirmed;
   @Column(nullable = false, unique = true)
   private String userName;
   @Column(nullable = false)
   private byte[] passWord;
   @Column(nullable = false)
   private Character status;
   @Column
   private String confirmSecret;
   @Column
   private Character userRole;
   @Column(nullable = false)
   private Boolean enabled;
   @Temporal(javax.persistence.TemporalType.TIMESTAMP)
   private Date lastLogin;
   // relations
   @OneToMany(mappedBy = "user")
   private List<Posting> postings = new ArrayList<Posting>();

   //
   // getter/setter
   //
   public String getLabel() {
      return displayName;
   }

   public String getDisplayName() {
      return displayName;
   }

   public String getEmail() {
      return email;
   }

   public void setEmail(String email) {
      this.email = email;
   }

   public String getEmailConfirmed() {
      return emailConfirmed;
   }

   public String getUserName() {
      return userName;
   }

   public void setUserName(String userName) {
      this.userName = userName;
   }

   public String getConfirmSecret() {
      return confirmSecret;
   }

   public UserRole getUserRole() {
      return UserRole.getRoleById(userRole);
   }

   public void setUserRole(UserRole userRole) {
      this.userRole = userRole.getRoleId();
   }

   public boolean isEnabled() {
      return enabled;
   }

   public Date getLastLogin() {
      return lastLogin;
   }

   public void setLastLogin(Date lastLogin) {
      this.lastLogin = lastLogin;
   }

   public List<Posting> getPostings() {
      return postings;
   }

   //
   // Registration functions
   //
   public void setRegistered(String displayName, String email, String userName, String passWord) {
      log.info("setRegistered");
      // has to be a new user object
      assert (this.status == null);
      this.displayName = displayName;
      this.email = email;
      this.userName = userName;
      this.passWord = encodePassWord(passWord);
      //
      this.enabled = true;
      this.status = STATUS_REGISTRATION_PENDING;
      this.confirmSecret = createConfirmSecret();
      this.userRole = UserRole.getBloggerRole().getRoleId();
   }

   public boolean checkRegisterConfirm() {
      return status.equals(STATUS_REGISTRATION_PENDING);
   }

   public void setRegisterConfirmed() {
      log.info("setRegisterConfirmed");
      this.status = STATUS_CONFIRMED;
      this.confirmSecret = null;
      this.emailConfirmed = this.email;
   }

   //
   // Reset password functions
   //
   public void setPasswordReset() {
      log.info("setPasswordReset");
      // if the user has an pending email change the change is reseted now
      if (this.status.equals(STATUS_EMAIL_CHANGE_PENDIG)) {
         this.email = this.emailConfirmed;
      }
      this.status = STATUS_PASSWORD_RESET_PENDING;
      this.confirmSecret = createConfirmSecret();
   }

   public boolean checkPasswordResetConfirm() {
      return status.equals(STATUS_PASSWORD_RESET_PENDING);
   }

   // set the password in the user object, change state
   public void passwordResetComplete(String passWord) {
      this.status = STATUS_CONFIRMED;
      this.confirmSecret = null;
      this.passWord = encodePassWord(passWord);
   }

   //
   // Login functions
   //
   public boolean userLogIn(String passWord) {
      if (status.equals(STATUS_REGISTRATION_PENDING) || !enabled) {
         log.debug("wrong user state");
         return false;
      }
      if (!verifyPassWord(passWord)) {
         log.debug("wrong password");
         return false;
      }
      lastLogin = new Date();
      // as the user loged in successfully the password reset state is not needed any longer
      if (status.equals(STATUS_PASSWORD_RESET_PENDING)) {
         status = STATUS_CONFIRMED;
         confirmSecret = null;
      }
      return true;
   }

   //
   // Data change
   //
   public void changeData(String displayName, String email, String userName, String passWord) {
      log.info("changeData - start");
      this.displayName = displayName;
      if (!email.equals(this.email)) {
         this.status = STATUS_EMAIL_CHANGE_PENDIG;
         this.confirmSecret = createConfirmSecret();
         this.email = email;
      }
      this.email = email;
      this.userName = userName;
      if (!passWord.isEmpty()) {
         this.passWord = encodePassWord(passWord);
      }
      //
      log.debug("changeData - end");
   }

   public boolean checkEmailConfirm() {
      return status.equals(STATUS_EMAIL_CHANGE_PENDIG);
   }

   public void setEmailConfirmed() {
      this.status = STATUS_CONFIRMED;
      this.confirmSecret = null;
      this.emailConfirmed = this.email;
   }

   //
   // helper functions
   //
   private String createConfirmSecret() {
      return MiscUtils.encodeBase64UrlSafe(MiscUtils.getRandom(confirmSecretLen));
   }

   private static byte[] encodePassWord(String passWord) {
      assert (passWord != null && !passWord.isEmpty());
      // set password hash with salt
      String environmentRandom = "";
      if (JsfUtil.getSessionOrNull() != null) {
         environmentRandom = JsfUtil.getSession().getId();
      }
      environmentRandom += Long.toHexString(System.currentTimeMillis());
      environmentRandom += Long.toHexString(System.nanoTime());
      byte[] salt = MiscUtils.getSalt(environmentRandom, saltLen);
      byte[] hash = MiscUtils.getDigest(passWord, salt);
      byte[] passWordBytes = MiscUtils.arrayConcat(hash, salt);
      //
      log.trace("passWord:     " + passWord);
      log.trace("hash hex:     " + MiscUtils.encodeHex(hash));
      log.trace("salt hex:     " + MiscUtils.encodeHex(salt));
      log.trace("password hex: " + MiscUtils.encodeHex(passWordBytes));
      return passWordBytes;
   }

   private boolean verifyPassWord(String passWord) {
      // last 4 byte carray the salt
      byte[] salt = Arrays.copyOfRange(this.passWord, this.passWord.length - saltLen, this.passWord.length);
      // the first part up to the last 4 byte is the stored password hash
      byte[] storedHash = Arrays.copyOf(this.passWord, this.passWord.length - saltLen);
      // the computed hash of the provided password concatenated with the salt
      byte[] computedHash = MiscUtils.getDigest(passWord, salt);

      log.trace("salt hex:         " + MiscUtils.encodeHex(salt));
      log.trace("storedHash hex:   " + MiscUtils.encodeHex(storedHash));
      log.trace("computedHash hex: " + MiscUtils.encodeHex(computedHash));

      return Arrays.equals(storedHash, computedHash);
   }
}
